// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;

use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::parking_lot::RwLock;
use deno_lockfile::NpmPackageDependencyLockfileInfo;
use deno_lockfile::NpmPackageLockfileInfo;
use deno_npm::registry::NpmPackageInfo;
use deno_npm::registry::NpmPackageVersionDistInfoIntegrity;
use deno_npm::registry::NpmRegistryApi;
use deno_npm::resolution::NpmPackageVersionResolutionError;
use deno_npm::resolution::NpmPackagesPartitioned;
use deno_npm::resolution::NpmResolutionError;
use deno_npm::resolution::NpmResolutionSnapshot;
use deno_npm::resolution::NpmResolutionSnapshotPendingResolver;
use deno_npm::resolution::NpmResolutionSnapshotPendingResolverOptions;
use deno_npm::resolution::PackageCacheFolderIdNotFoundError;
use deno_npm::resolution::PackageNotFoundFromReferrerError;
use deno_npm::resolution::PackageNvNotFoundError;
use deno_npm::resolution::PackageReqNotFoundError;
use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot;
use deno_npm::NpmPackageCacheFolderId;
use deno_npm::NpmPackageId;
use deno_npm::NpmResolutionPackage;
use deno_npm::NpmSystemInfo;
use deno_semver::package::PackageNv;
use deno_semver::package::PackageReq;
use deno_semver::VersionReq;

use crate::args::Lockfile;
use crate::util::sync::TaskQueue;

use super::CliNpmRegistryApi;

/// Handles updating and storing npm resolution in memory where the underlying
/// snapshot can be updated concurrently. Additionally handles updating the lockfile
/// based on changes to the resolution.
///
/// This does not interact with the file system.
pub struct NpmResolution {
  api: Arc<CliNpmRegistryApi>,
  snapshot: RwLock<NpmResolutionSnapshot>,
  update_queue: TaskQueue,
  maybe_lockfile: Option<Arc<Mutex<Lockfile>>>,
}

impl std::fmt::Debug for NpmResolution {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    let snapshot = self.snapshot.read();
    f.debug_struct("NpmResolution").field("snapshot", &snapshot.as_valid_serialized().as_serialized()).finish()
  }
}

impl NpmResolution {
  pub fn from_serialized(api: Arc<CliNpmRegistryApi>, initial_snapshot: Option<ValidSerializedNpmResolutionSnapshot>, maybe_lockfile: Option<Arc<Mutex<Lockfile>>>) -> Self {
    let snapshot = NpmResolutionSnapshot::new(initial_snapshot.unwrap_or_default());
    Self::new(api, snapshot, maybe_lockfile)
  }

  pub fn new(api: Arc<CliNpmRegistryApi>, initial_snapshot: NpmResolutionSnapshot, maybe_lockfile: Option<Arc<Mutex<Lockfile>>>) -> Self {
    Self {
      api,
      snapshot: RwLock::new(initial_snapshot),
      update_queue: Default::default(),
      maybe_lockfile,
    }
  }

  pub async fn add_package_reqs(&self, package_reqs: &[PackageReq]) -> Result<(), AnyError> {
    // only allow one thread in here at a time
    let _permit = self.update_queue.acquire().await;
    let snapshot = add_package_reqs_to_snapshot(&self.api, package_reqs, self.maybe_lockfile.clone(), || self.snapshot.read().clone()).await?;

    *self.snapshot.write() = snapshot;
    Ok(())
  }

  pub async fn set_package_reqs(&self, package_reqs: &[PackageReq]) -> Result<(), AnyError> {
    // only allow one thread in here at a time
    let _permit = self.update_queue.acquire().await;

    let reqs_set = package_reqs.iter().collect::<HashSet<_>>();
    let snapshot = add_package_reqs_to_snapshot(&self.api, package_reqs, self.maybe_lockfile.clone(), || {
      let snapshot = self.snapshot.read().clone();
      let has_removed_package = !snapshot.package_reqs().keys().all(|req| reqs_set.contains(req));
      // if any packages were removed, we need to completely recreate the npm resolution snapshot
      if has_removed_package {
        snapshot.into_empty()
      } else {
        snapshot
      }
    })
    .await?;

    *self.snapshot.write() = snapshot;

    Ok(())
  }

  pub async fn resolve_pending(&self) -> Result<(), AnyError> {
    // only allow one thread in here at a time
    let _permit = self.update_queue.acquire().await;

    let snapshot = add_package_reqs_to_snapshot(&self.api, &Vec::new(), self.maybe_lockfile.clone(), || self.snapshot.read().clone()).await?;

    *self.snapshot.write() = snapshot;

    Ok(())
  }

  pub fn resolve_pkg_cache_folder_id_from_pkg_id(&self, id: &NpmPackageId) -> Option<NpmPackageCacheFolderId> {
    self.snapshot.read().package_from_id(id).map(|p| p.get_package_cache_folder_id())
  }

  pub fn resolve_pkg_id_from_pkg_cache_folder_id(&self, id: &NpmPackageCacheFolderId) -> Result<NpmPackageId, PackageCacheFolderIdNotFoundError> {
    self.snapshot.read().resolve_pkg_from_pkg_cache_folder_id(id).map(|pkg| pkg.id.clone())
  }

  pub fn resolve_package_from_package(&self, name: &str, referrer: &NpmPackageCacheFolderId) -> Result<NpmResolutionPackage, Box<PackageNotFoundFromReferrerError>> {
    self.snapshot.read().resolve_package_from_package(name, referrer).cloned()
  }

  /// Resolve a node package from a deno module.
  pub fn resolve_pkg_id_from_pkg_req(&self, req: &PackageReq) -> Result<NpmPackageId, PackageReqNotFoundError> {
    self.snapshot.read().resolve_pkg_from_pkg_req(req).map(|pkg| pkg.id.clone())
  }

  pub fn resolve_pkg_reqs_from_pkg_id(&self, id: &NpmPackageId) -> Vec<PackageReq> {
    let snapshot = self.snapshot.read();
    let mut pkg_reqs = snapshot.package_reqs().iter().filter(|(_, nv)| *nv == &id.nv).map(|(req, _)| req.clone()).collect::<Vec<_>>();
    pkg_reqs.sort(); // be deterministic
    pkg_reqs
  }

  pub fn resolve_pkg_id_from_deno_module(&self, id: &PackageNv) -> Result<NpmPackageId, PackageNvNotFoundError> {
    self.snapshot.read().resolve_package_from_deno_module(id).map(|pkg| pkg.id.clone())
  }

  /// Resolves a package requirement for deno graph. This should only be
  /// called by deno_graph's NpmResolver or for resolving packages in
  /// a package.json
  pub fn resolve_pkg_req_as_pending(&self, pkg_req: &PackageReq) -> Result<PackageNv, NpmPackageVersionResolutionError> {
    // we should always have this because it should have been cached before here
    let package_info = self.api.get_cached_package_info(&pkg_req.name).unwrap();
    self.resolve_pkg_req_as_pending_with_info(pkg_req, &package_info)
  }

  /// Resolves a package requirement for deno graph. This should only be
  /// called by deno_graph's NpmResolver or for resolving packages in
  /// a package.json
  pub fn resolve_pkg_req_as_pending_with_info(&self, pkg_req: &PackageReq, package_info: &NpmPackageInfo) -> Result<PackageNv, NpmPackageVersionResolutionError> {
    debug_assert_eq!(pkg_req.name, package_info.name);
    let mut snapshot = self.snapshot.write();
    let pending_resolver = get_npm_pending_resolver(&self.api);
    let nv = pending_resolver.resolve_package_req_as_pending(&mut snapshot, pkg_req, package_info)?;
    Ok(nv)
  }

  pub fn package_reqs(&self) -> HashMap<PackageReq, PackageNv> {
    self.snapshot.read().package_reqs().clone()
  }

  pub fn all_system_packages(&self, system_info: &NpmSystemInfo) -> Vec<NpmResolutionPackage> {
    self.snapshot.read().all_system_packages(system_info)
  }

  pub fn all_system_packages_partitioned(&self, system_info: &NpmSystemInfo) -> NpmPackagesPartitioned {
    self.snapshot.read().all_system_packages_partitioned(system_info)
  }

  pub fn snapshot(&self) -> NpmResolutionSnapshot {
    self.snapshot.read().clone()
  }

  pub fn serialized_valid_snapshot(&self) -> ValidSerializedNpmResolutionSnapshot {
    self.snapshot.read().as_valid_serialized()
  }

  pub fn serialized_valid_snapshot_for_system(&self, system_info: &NpmSystemInfo) -> ValidSerializedNpmResolutionSnapshot {
    self.snapshot.read().as_valid_serialized_for_system(system_info)
  }

  pub fn lock(&self, lockfile: &mut Lockfile) -> Result<(), AnyError> {
    let snapshot = self.snapshot.read();
    populate_lockfile_from_snapshot(lockfile, &snapshot)
  }
}

async fn add_package_reqs_to_snapshot(api: &CliNpmRegistryApi, package_reqs: &[PackageReq], maybe_lockfile: Option<Arc<Mutex<Lockfile>>>, get_new_snapshot: impl Fn() -> NpmResolutionSnapshot) -> Result<NpmResolutionSnapshot, AnyError> {
  let snapshot = get_new_snapshot();
  let snapshot = if !snapshot.has_pending() && package_reqs.iter().all(|req| snapshot.package_reqs().contains_key(req)) {
    log::debug!("Snapshot already up to date. Skipping pending resolution.");
    snapshot
  } else {
    let pending_resolver = get_npm_pending_resolver(api);
    let result = pending_resolver.resolve_pending(snapshot, package_reqs).await;
    api.clear_memory_cache();
    match result {
      Ok(snapshot) => snapshot,
      Err(NpmResolutionError::Resolution(err)) if api.mark_force_reload() => {
        log::debug!("{err:#}");
        log::debug!("npm resolution failed. Trying again...");

        // try again
        let snapshot = get_new_snapshot();
        let result = pending_resolver.resolve_pending(snapshot, package_reqs).await;
        api.clear_memory_cache();
        // now surface the result after clearing the cache
        result?
      }
      Err(err) => return Err(err.into()),
    }
  };

  if let Some(lockfile_mutex) = maybe_lockfile {
    let mut lockfile = lockfile_mutex.lock();
    populate_lockfile_from_snapshot(&mut lockfile, &snapshot)?;
  }

  Ok(snapshot)
}

fn get_npm_pending_resolver(api: &CliNpmRegistryApi) -> NpmResolutionSnapshotPendingResolver<CliNpmRegistryApi> {
  NpmResolutionSnapshotPendingResolver::new(NpmResolutionSnapshotPendingResolverOptions {
    api,
    // WARNING: When bumping this version, check if anything needs to be
    // updated in the `setNodeOnlyGlobalNames` call in 99_main_compiler.js
    types_node_version_req: Some(VersionReq::parse_from_npm("18.0.0 - 18.16.19").unwrap()),
  })
}

fn populate_lockfile_from_snapshot(lockfile: &mut Lockfile, snapshot: &NpmResolutionSnapshot) -> Result<(), AnyError> {
  for (package_req, nv) in snapshot.package_reqs() {
    lockfile.insert_package_specifier(format!("npm:{}", package_req), format!("npm:{}", snapshot.resolve_package_from_deno_module(nv).unwrap().id.as_serialized()));
  }
  for package in snapshot.all_packages_for_every_system() {
    lockfile.check_or_insert_npm_package(npm_package_to_lockfile_info(package))?;
  }
  Ok(())
}

fn npm_package_to_lockfile_info(pkg: &NpmResolutionPackage) -> NpmPackageLockfileInfo {
  fn integrity_for_lockfile(integrity: NpmPackageVersionDistInfoIntegrity) -> String {
    match integrity {
      NpmPackageVersionDistInfoIntegrity::Integrity { algorithm, base64_hash } => format!("{}-{}", algorithm, base64_hash),
      NpmPackageVersionDistInfoIntegrity::UnknownIntegrity(integrity) => integrity.to_string(),
      NpmPackageVersionDistInfoIntegrity::LegacySha1Hex(hex) => hex.to_string(),
    }
  }

  let dependencies = pkg.dependencies.iter().map(|(name, id)| NpmPackageDependencyLockfileInfo { name: name.clone(), id: id.as_serialized() }).collect();

  NpmPackageLockfileInfo {
    display_id: pkg.id.nv.to_string(),
    serialized_id: pkg.id.as_serialized(),
    integrity: integrity_for_lockfile(pkg.dist.integrity()),
    dependencies,
  }
}
